package com.rabbit.eap.system.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.rabbit.eap.config.constant.CommonRedisKey;
import com.rabbit.eap.config.properties.EapProperties;
import com.rabbit.eap.config.properties.JwtProperties;
import com.rabbit.eap.framework.common.api.ApiResult;
import com.rabbit.eap.framework.common.enums.MsgEnum;
import com.rabbit.eap.framework.shiro.cache.LoginRedisService;
import com.rabbit.eap.framework.shiro.jwt.JwtToken;
import com.rabbit.eap.framework.shiro.util.JwtTokenUtil;
import com.rabbit.eap.framework.shiro.util.JwtUtil;
import com.rabbit.eap.framework.shiro.util.SaltUtil;
import com.rabbit.eap.framework.shiro.vo.LoginSysUserVO;
import com.rabbit.eap.framework.utils.IpUtil;
import com.rabbit.eap.framework.utils.PasswordUtil;
import com.rabbit.eap.system.component.SysPermissionService;
import com.rabbit.eap.system.entity.SysUser;
import com.rabbit.eap.system.entity.SysUserRole;
import com.rabbit.eap.system.enums.StateEnum;
import com.rabbit.eap.system.exception.VerificationCodeException;
import com.rabbit.eap.system.mapper.SysUserMapper;
import com.rabbit.eap.system.param.LoginParam;
import com.rabbit.eap.system.service.ISysUserRoleService;
import com.rabbit.eap.system.service.LoginService;
import com.rabbit.eap.system.vo.LoginSysUserTokenVO;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;

import javax.servlet.http.HttpServletRequest;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * @projectName eap
 * @packageName com.rabbit.eap.system.service.impl
 * @className LoginServiceImpl
 * @author： rabbit
 * @version： 1.0
 * @since： 2020/5/30 16:39
 * <p>Description: LoginServiceImpl
 * 登录服务实现类
 * </p>

 */
@Api
@Slf4j
@Service
public class LoginServiceImpl  implements LoginService {
    @Lazy
    @Autowired
    private LoginRedisService loginRedisService;

    @Lazy
    @Autowired
    private JwtProperties jwtProperties;

    @Lazy
    @Autowired
    private SysUserMapper sysUserMapper;

    @Autowired
    private SysPermissionService sysPermissionService;

    @Lazy
    @Autowired
    private ISysUserRoleService sysUserRoleService;

    @Lazy
    @Autowired
    private EapProperties eapProperties;

    @Lazy
    @Autowired
    private RedisTemplate redisTemplate;
    /**
     * @Description 系统登录
     * @methodName: login
     * @param loginParam:
     * @return: com.rabbit.eap.system.vo.LoginSysUserTokenVo
     * @date: 2020/5/31 20:26
     * @author: rabbit
     *
     */
    @Override
    public ApiResult login(LoginParam loginParam) throws Exception {
        String username = loginParam.getUsername();
        // 校验验证码
        checkVerifyCode(loginParam.getVerifyToken(), loginParam.getCode());
        // 从数据库中获取登录用户信息
        SysUser sysUser = getSysUserByUsername(username);
        if (ObjectUtils.isEmpty(sysUser)) {
            log.error("登录失败,loginParam:{}", loginParam);
            return ApiResult.fail(MsgEnum.USERNAME_OR_PWD_WRONG.getDesc());
        }
        if(ObjectUtils.nullSafeEquals(sysUser.getStatus(),StateEnum.DISABLE.getCode())) {
            return ApiResult.fail(MsgEnum.USER_HAS_BEAN_SHUTDOWN.getDesc());
        }
        if(ObjectUtils.nullSafeEquals(sysUser.getStatus(),StateEnum.DELETED.getCode())) {
            return ApiResult.fail(MsgEnum.USER_IS_EMPTY.getDesc());
        }
        // 实际项目中，前端传过来的密码应先加密
        // 原始密码明文：123456
        // 原始密码前端加密：sha256(123456)
        // 后台加密规则：sha256(sha256(123456) + salt)
        String encryptPassword = PasswordUtil.encrypt(loginParam.getPassword(), sysUser.getSalt());
        if (!encryptPassword.equals(sysUser.getPassword())) {
            return ApiResult.fail(MsgEnum.USERNAME_OR_PWD_WRONG.getDesc());
        }
        QueryWrapper<SysUserRole> query = new QueryWrapper<>();
        query.eq("user_id", sysUser.getId());
        List<SysUserRole> roleList = sysUserRoleService.list(query);
        if(CollectionUtils.isEmpty(roleList)){
            return ApiResult.fail(MsgEnum.USER_HAS_NO_MENU_AUTHORIZATION.getDesc());
        }

        // 获取数据库中保存的盐值
        String newSalt = SaltUtil.getSalt(sysUser.getSalt(), jwtProperties);

        // 生成token字符串并返回
        Long expireSecond = jwtProperties.getExpireSecond();
        String token = JwtUtil.generateToken(username, newSalt, Duration.ofSeconds(expireSecond));
        log.debug("token:{}", token);

        // 创建AuthenticationToken
        JwtToken jwtToken = JwtToken.build(token, username, newSalt, expireSecond);

        boolean enableShiro = eapProperties.getShiro().isEnable();
        if (enableShiro) {
            // 从SecurityUtils里边创建一个 subject
            Subject subject = SecurityUtils.getSubject();
            // 执行认证登录
            subject.login(jwtToken);
        } else {
            log.warn("未启用Shiro");
        }

        LoginSysUserVO loginSysUser = sysPermissionService.getLoginSysUser(sysUser);
        // 缓存登录信息到Redis
        loginRedisService.cacheLoginInfo(jwtToken, loginSysUser);
        log.debug("登录成功,username:{}", username);
        SysUser user = new SysUser().setId(sysUser.getId()).setLoginIp(IpUtil.getRequestIp()).setLoginDate(LocalDateTime.now());
        sysUserMapper.updateById(user);

        // 缓存登录信息到redis
        String tokenSha256 = DigestUtils.sha256Hex(token);
        redisTemplate.opsForValue().set(tokenSha256, loginSysUser, expireSecond, TimeUnit.SECONDS);

        // 返回token和登录用户信息对象
        LoginSysUserTokenVO loginSysUserTokenVo = new LoginSysUserTokenVO();
        loginSysUserTokenVo.setExpire(expireSecond);
        loginSysUserTokenVo.setToken(token);
        loginSysUserTokenVo.setUserId(sysUser.getId());
        loginSysUserTokenVo.setUserName(sysUser.getUserName());
        return ApiResult.ok(loginSysUserTokenVo,"登录成功");
    }
    /**
     * @Description  系统退出
     * @methodName: logout
     * @param request:
     * @return: void
     * @date: 2020/5/31 20:26
     * @author: rabbit
     *
     */
    @Override
    public void logout(HttpServletRequest request) throws Exception {
        Subject subject = SecurityUtils.getSubject();
        //注销
        subject.logout();
        // 获取token
        String token = JwtTokenUtil.getToken(request);
        String username = JwtUtil.getUsername(token);
        // 删除Redis缓存信息
        loginRedisService.deleteLoginInfo(token, username);
        log.info("登出成功,username:{},token:{}", username, token);
    }
    /**
     * @Description 校验验证码
     * @methodName: checkVerifyCode
     * @param verifyToken:
     * @param code:
     * @return: void
     * @date: 2020/5/31 20:27
     * @author: rabbit
     *
     */
    @Override
    public void checkVerifyCode(String verifyToken, String code) throws Exception {
        // 如果没有启用验证码，则返回
        if (!eapProperties.isEnableVerifyCode()) {
            return;
        }
        // 校验验证码
        if (StringUtils.isBlank(code)) {
            throw new VerificationCodeException("请输入验证码");
        }
        // 从redis中获取
        String redisKey = String.format(CommonRedisKey.VERIFY_CODE, verifyToken);
        String generateCode = (String) redisTemplate.opsForValue().get(redisKey);
        if (StringUtils.isBlank(generateCode)) {
            throw new VerificationCodeException("验证码已过期或不正确");
        }
        // 不区分大小写
        if (!generateCode.equalsIgnoreCase(code)) {
            throw new VerificationCodeException("验证码错误");
        }
        // 验证码校验成功，删除Redis缓存
        redisTemplate.delete(redisKey);
    }
    @Override
    public SysUser getSysUserByUsername(String username) {
        SysUser sysUser = new SysUser().setUserName(username);
        return sysUserMapper.selectOne(new QueryWrapper<SysUser>(sysUser));
    }
}
